using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

namespace Oops.Xna.Framework.Graphics
{
	public class InstancedModelMesh<T> where T : InstancedData 
	{
		const int MAX_INSTANCES = 40;

		const int SizeOfSingle = sizeof(float);
		const int SizeOfVector4 = sizeof(float) * 4;
		const int SizeOfMatrix = sizeof(float) * 16;

		private InstancedModel<T> model;

		private GraphicsDevice graphicsDevice;
		private ModelMesh mesh;

		private int vertexCount;
		private int vertexStride;
		private VertexBuffer vertexBuffer;
		private VertexDeclaration vertexDeclaration;

		private int indexCount;
		private IndexBuffer indexBuffer;

		private int maxInstances;

		private Texture2D animationTexture;
#if !XBOX
		private VertexElement[] originalVertexDeclaration;
		private bool vertexDataIsReplicated;
#else
		private T tempInstanceData;		
#endif
		internal InstancedModelMesh(GraphicsDevice graphicsDevice, ModelMesh modeMesh, Model model)
		{
			//this.model = model;

			this.graphicsDevice = graphicsDevice;
			this.mesh = modeMesh;
			this.animationTexture = (model.Tag as SkinningData).AnimationTexture;

			// We are assuming all the mesh parts have the same vertex stride
			this.vertexCount = this.mesh.VertexBuffer.SizeInBytes / mesh.MeshParts[0].VertexStride;
			this.indexCount = this.mesh.IndexBuffer.SizeInBytes / (this.mesh.IndexBuffer.IndexElementSize == IndexElementSize.SixteenBits ? 2 : 4);
			this.indexBuffer = this.mesh.IndexBuffer;

			// Calculate the actual number of instances
			// We're using ushort for our instances, so we can only reference up to ushort.MaxValue
			this.maxInstances = Math.Min(ushort.MaxValue / this.vertexCount, InstancedModelMesh<T>.MAX_INSTANCES);
#if XBOX
			this.vertexDeclaration = this.mesh.MeshParts[0].VertexDeclaration;
			this.ReplicateIndexData();
#else
			// On Windows, store a copy of the original vertex declaration.
			this.originalVertexDeclaration = this.mesh.MeshParts[0].VertexDeclaration.GetVertexElements();
#endif
		}

		internal protected InstancedModel<T> Model
		{
			get { return this.model; }
			set 
			{ 
				if (this.model != value)
				{
					// Hold onto the property value
					this.model = value; 
#if XBOX
					// Create temporary instance data for batching.
					this.tempInstanceData = value.CreateInstancedData(this.maxInstances); 
#endif
				}
			}
		}
#if !XBOX
		internal void SetInstancingTechnique(T instanceData)
		{
			VertexElement[] extraElements = new VertexElement[0];

			short stream = 1;
			byte usageIndex = 1;
			for (int i = 0; i < instanceData.Properties.Count; i++)
			{
				int length = extraElements.Length;

				VertexElement[] elements = instanceData.Properties[i].GetVertexDeclaration(stream++, ref usageIndex);

				Array.Resize<VertexElement>(ref extraElements, extraElements.Length + elements.Length);
				Array.Copy(elements, 0, extraElements, length, elements.Length);
			}

			this.ExtendVertexDeclaration(extraElements);
		}

		/// <summary>
		/// Windows only. Modifies our vertex declaration to include additional
		/// vertex input channels. This is necessary when switching between the
		/// ShaderInstancing and HardwareInstancing techniques.
		/// </summary>
		private void ExtendVertexDeclaration(VertexElement[] extraElements)
		{
			// Get rid of the existing vertex declaration.
			if (this.vertexDeclaration != null)
			{
				this.vertexDeclaration.Dispose();
			}

			// Append the new elements to the original format.
			int length = this.originalVertexDeclaration.Length + extraElements.Length;

			VertexElement[] elements = new VertexElement[length];
			this.originalVertexDeclaration.CopyTo(elements, 0);
			extraElements.CopyTo(elements, this.originalVertexDeclaration.Length);

			// Create a new vertex declaration.
			this.vertexDeclaration = new VertexDeclaration(this.graphicsDevice, elements);
		}
#endif 
		private void ReplicateIndexData()
		{
			List<ushort> indices = new List<ushort>();

			int size = this.mesh.IndexBuffer.IndexElementSize == IndexElementSize.SixteenBits ? 2 : 4;
			this.indexCount = this.mesh.IndexBuffer.SizeInBytes / size;

			ushort[] oldIndices = new ushort[this.indexCount];

			this.mesh.IndexBuffer.GetData(oldIndices);
			this.mesh.IndexBuffer.Dispose();

			foreach (ModelMeshPart meshPart in this.mesh.MeshParts)
			{
				// Replicate one copy of the original index buffer for each instance.
				for (int j = 0; j < this.maxInstances; j++)
				{
					int instanceOffset = j * this.vertexCount;

					// Basically, keep adding the indices. We have to replicate this ModelMeshPart index for each instance we can have
					// Note that each time we are incrementing by instanceOffset. This is so in the vertex shader, when we divide
					// by the vertex count, we know exactly which index we are at.
					for (int i = meshPart.StartIndex; i < meshPart.StartIndex + (meshPart.PrimitiveCount * 3); i++)
					{
						indices.Add((ushort)(oldIndices[i] + instanceOffset));
					}
				}
			}

			// Create a new index buffer, and set the replicated data into it.
			this.indexBuffer = new IndexBuffer(
				this.graphicsDevice,
				sizeof(ushort) * indices.Count,
				BufferUsage.None,
				IndexElementSize.SixteenBits);
			this.indexBuffer.SetData(indices.ToArray());
		}

		public void Draw(InstancedData data, ref Matrix view, ref Matrix projection)
		{
			// Set the graphics device to use our vertex data
			this.graphicsDevice.Indices = this.indexBuffer;
			this.graphicsDevice.VertexDeclaration = this.vertexDeclaration;

			// Iterate on mesh parts.
			for (int i = 0; i < this.mesh.MeshParts.Count; i++)
			{
				// Get the current mesh part.
				ModelMeshPart part = this.mesh.MeshParts[i];

				// Set the vertex information on the graphics device.
#if XBOX
				this.graphicsDevice.VertexDeclaration = part.VertexDeclaration;
#endif
				this.graphicsDevice.Vertices[0].SetSource(this.mesh.VertexBuffer, part.StreamOffset, part.VertexStride);

				// Set effect parameters.
				part.Effect.Parameters["View"].SetValue(view);
				part.Effect.Parameters["Projection"].SetValue(projection);
#if XBOX
				part.Effect.Parameters["VertexCount"].SetValue(this.vertexCount);
#endif
				part.Effect.Parameters["AnimationTexture"].SetValue(this.animationTexture);
				part.Effect.Parameters["DiffuseColor"].SetValue(Color.White.ToVector3());
				//part.Effect.Parameters["BoneDelta"].SetValue(1.0f / this.animationTexture.Width);
				//part.Effect.Parameters["RowDelta"].SetValue(1.0f / this.animationTexture.Height);

				// Draw the instances.
				part.Effect.Begin();
				for (int j = 0; j < part.Effect.CurrentTechnique.Passes.Count; j++)
				{
					// Get the current effect pass.
					EffectPass pass = part.Effect.CurrentTechnique.Passes[j];

					pass.Begin();
#if XBOX
					// Iterate on batches.
					for (int k = 0; k < data.Size; k += this.maxInstances)
					{
						// Determine this batch's instance count.
						int instanceCount = data.Size - k;
						if (instanceCount > this.maxInstances)
						{
							instanceCount = this.maxInstances;
						}

						// Copy the instance data to temporary storage and assign to effect parameters.
						for (int m = 0; m < data.Properties.Count; m++)
						{
							Array.Copy(
								(Array)data.Properties[m].GetValue(), 
								k, 
								(Array)this.tempInstanceData.Properties[m].GetValue(), 
								0, 
								instanceCount);
							data.Properties[m].SetEffectParameterValue(
								part.Effect.Parameters[data.Properties[m].Name], 
								this.tempInstanceData.Properties[m].GetValue());
						}
						part.Effect.CommitChanges();

						// Draw this batch's instances.
						this.graphicsDevice.DrawIndexedPrimitives(
							PrimitiveType.TriangleList,
							part.BaseVertex,
							0,
							part.NumVertices * instanceCount,
							part.StartIndex,
							part.PrimitiveCount * instanceCount);
					}
#else
					// Get the vertex stream collection from the graphics device.
					VertexStreamCollection vertices = this.graphicsDevice.Vertices;

					// Set the frequence of index data on the first stream.
					vertices[0].SetFrequencyOfIndexData(data.Size);

					// Set the instance streams to the correct positions and set frequency of instance data.
					for (int k = 0; k < data.Properties.Count; k++)
					{
						data.Properties[k].SetDataStream(this.model.InstanceStreams[k]);

						vertices[k + 1].SetSource(this.model.InstanceStreams[k], 0, data.Properties[k].SizeOf);
						vertices[k + 1].SetFrequencyOfInstanceData(1);
					}

					// Draw all instances in one call.
					this.graphicsDevice.DrawIndexedPrimitives(
						PrimitiveType.TriangleList,
						part.BaseVertex,
						0,
						part.NumVertices,
						part.StartIndex,
						part.PrimitiveCount);

					// Clear vertex streams.
					vertices[0].SetSource(null, 0, 0);
					for (int k = 0; k <= data.Properties.Count - 1; k++)
					{
						vertices[k + 1].SetSource(null, 0, 0);
					}
#endif
					pass.End();
				}

				part.Effect.End();
			}
		}
	}
}
